use raylib::prelude::*;

// Define screen dimensions
const WIDTH: i32 = 800;
const HEIGHT: i32 = 600;

// Define a custom background color
static BACKGROUND_COLOR: Color = Color::BLACK;

/// Represents a Quaternion for 3D rotations.
#[derive(Clone, Copy)]
pub struct Quaternion {
    pub a: f32,
    pub b: f32,
    pub c: f32,
    pub d: f32,
}

impl Quaternion {
    /// Creates a new Quaternion.
    pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self {
        Self { a, b, c, d }
    }

    /// Creates a pure Quaternion from a 3D vector.
    pub fn from_v3(v3: [f32; 3]) -> Self {
        let [x, y, z] = v3;
        Self {
            a: 0., // Real part is 0 for a pure quaternion
            b: x,
            c: y,
            d: z,
        }
    }

    /// Converts a pure Quaternion back to a 3D vector.
    pub fn to_v3(self) -> [f32; 3] {
        // Assert that the real part is close to zero, indicating it's a pure quaternion
        assert!(f32::abs(self.a) < 1e-6);
        [self.b, self.c, self.d]
    }

    /// Adds two quaternions.
    pub fn add(self, q: &Quaternion) -> Self {
        Self {
            a: self.a + q.a,
            b: self.b + q.b,
            c: self.c + q.c,
            d: self.d + q.d,
        }
    }

    /// Scales a quaternion by a scalar.
    pub fn scale(self, s: f32) -> Self {
        Self {
            a: self.a * s,
            b: self.b * s,
            c: self.c * s,
            d: self.d * s,
        }
    }

    /// Calculates the product of two quaternions (Hamilton product).
    fn product(self,
               Quaternion { a: a2, b: b2, c: c2, d: d2 }: Quaternion) -> Quaternion {
        let Quaternion { a: a1, b: b1, c: c1, d: d1 } = self;
        Self {
            a: a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2,
            b: a1 * b2 + b1 * a2 + c1 * d2 - d1 * c2,
            c: a1 * c2 - b1 * d2 + c1 * a2 + d1 * b2,
            d: a1 * d2 + b1 * c2 - c1 * b2 + d1 * a2,
        }
    }

    /// Calculates the inverse of a quaternion.
    pub fn inverse(self) -> Self {
        let Quaternion { a, b, c, d } = self;
        let q = a * a + b * b + c * c + d * d; // Norm squared
        Self {
            a: a / q,
            b: -b / q,
            c: -c / q,
            d: -d / q,
        }
    }

    /// Creates a rotation quaternion for a given axis and angle.
    /// The axis [x, y, z] should be normalized for correct behavior.
    pub fn rotate([x, y, z]: [f32; 3], theta: f32) -> Self {
        let l = (x * x + y * y + z * z).sqrt(); // Magnitude of the axis
        let c = (theta * 0.5).cos();
        let s = (theta * 0.5).sin();
        Self {
            a: c,
            b: x / l * s,
            c: y / l * s,
            d: z / l * s,
        }
    }
}

fn main() {
    // Initialize Raylib window
    let (mut rl, thread) = raylib::init()
        .size(WIDTH, HEIGHT)
        .resizable()
        .title("Hello Quaternions")
        .build();

    // Define the 8 vertices of a cube
    let vertices: [[f32; 3]; 8] = [
        // X     Y     Z
        [-1.0, 1.0, -1.0],  //0
        [1.0, 1.0, -1.0],   //1
        [1.0, 1.0, 1.0],    //2
        [-1.0, 1.0, 1.0],   //3
        [-1.0, -1.0, -1.0], //4
        [1.0, -1.0, -1.0],  //5
        [1.0, -1.0, 1.0],   //6
        [-1.0, -1.0, 1.0],  //7
    ];

    // Define the 12 lines (edges) that connect the vertices to form a cube
    let lines: [[usize; 2]; 12] = [
        [0, 1], [1, 2], [2, 3], [3, 0], // Top face
        [4, 5], [5, 6], [6, 7], [7, 4], // Bottom face
        [0, 4], [1, 5], [2, 6], [3, 7], // Connecting edges
    ];

    let mut theta = 0.0; // Angle for continuous rotation
    let mut distance = 30.0; // Current distance for perspective projection (zoom level)
    let mut target_distance = 30.0; // Target distance for smooth zooming
    let mut hue:f32=0.;

    rl.set_target_fps(60); // Set our game to run at 60 frames-per-second

    // Main game loop
    while !rl.window_should_close() {
        // Update logic
        // Get mouse wheel movement
        let wheel = rl.get_mouse_wheel_move();

        // Adjust the target distance based on mouse wheel movement
        if wheel < 0.0 {
            target_distance += 5.0; // Scroll down: move cube further away (zoom out)
        } else if wheel > 0.0 {
            target_distance -= 5.0; // Scroll up: move cube closer (zoom in)
        }

        // Ensure target_distance doesn't go too low to avoid division by zero or inverted projection
        if target_distance < 1.0 {
            target_distance = 1.0;
        }

        // Smoothly interpolate the current distance towards the target distance
        // The 0.1 is the smoothing factor. Smaller values make it smoother but slower.
        distance = distance + (target_distance - distance) * 0.1;


        let fwidth = rl.get_render_width() as f32;
        let fheight = rl.get_render_height() as f32;
        let scale = 1000.0; // Scaling factor for projection
        let SIZE = 10;      // Size of the circles drawn at vertices

        // Drawing
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(BACKGROUND_COLOR);

        for  repeater in 0..10 {

            hue=(hue+0.05)%360.;
            let color=Color::color_from_hsv(hue as f32,1.,1.).alpha(1.-(repeater as f32) /10.);

            let scale1=scale+(repeater as f32)*300. * (repeater as f32 /10.);
            /*// Draw vertices
            for i in 0..8 {
                let p1 = rotate_v3(vertices[i], theta); // Rotate the vertex
                // Project the 3D point to 2D, scale it, and convert to screen coordinates
                let [x, y] = to_screen(scale_v2(project(p1, distance), scale1), fwidth, fheight);
                d.draw_circle(x as i32, y as i32, SIZE as f32,color);
            }*/

            // Draw lines (edges)
            for [start, end] in lines.iter() {
                // Get rotated and projected coordinates for start and end points of each line
                let p1 = rotate_v3(vertices[*start], theta);
                let [x1, y1] = to_screen(scale_v2(project(p1, distance), scale1), fwidth, fheight);
                let p2 = rotate_v3(vertices[*end], theta);
                let [x2, y2] = to_screen(scale_v2(project(p2, distance), scale1), fwidth, fheight);

                d.draw_line(x1 as i32, y1 as i32, x2 as i32, y2 as i32, color);
            }
        }
        // Increment rotation angle for continuous animation
        theta += 0.02;
    }
}

/// Rotates a 3D vector using a quaternion.
/// The rotation is around an arbitrary axis (100, 100, 100) for simplicity.
pub fn rotate_v3(v3: [f32; 3], theta: f32) -> [f32; 3] {
    let p = Quaternion::from_v3(v3); // Convert vector to pure quaternion
    // Create a rotation quaternion for the given axis and angle
    let q = Quaternion::rotate([100.0, 50.0, 100.0], theta);
    // Apply rotation: q * p * q_inverse
    let result = q.product(p).product(q.inverse());
    result.to_v3() // Convert back to 3D vector
}

/*
  rotate (point) around y axis by theta
  x1= x*sin -y*cos
  y1= x*cos +y*sin
  (This function is not currently used in main, but provided in the original code)
*/
pub fn rotate_y([x, y, z]: [f32; 3], theta: f32) -> [f32; 3] {
    let cos_theta = theta.cos();
    let sin_theta = theta.sin();
    let x1 = x * cos_theta - z * sin_theta;
    let z1 = x * sin_theta + z * cos_theta;
    [x1, y, z1]
}

/*
  rotate (point) around x axis by theta
  (This function is not currently used in main, but provided in the original code)
*/
pub fn rotate_x([x, y, z]: [f32; 3], theta: f32) -> [f32; 3] {
    let cos_theta = theta.cos();
    let sin_theta = theta.sin();
    let y1 = y * cos_theta - z * sin_theta;
    let z1 = y * sin_theta + z * cos_theta;
    [x, y1, z1]
}

/// Projects a 3D point onto a 2D plane with perspective.
/// `distance` acts as the focal length or distance from the viewer.
pub fn project([x, y, z]: [f32; 3], distance: f32) -> [f32; 2] {
    [x / (z + distance), y / (z + distance)]
}

/// Converts normalized 2D coordinates to screen pixel coordinates.
pub fn to_screen([x, y]: [f32; 2], w: f32, h: f32) -> [f32; 2] {
    [w / 2.0 + x, h / 2.0 + y] // Center the origin and scale to screen size
}

/// Scales a 2D vector.
pub fn scale_v2([x, y]: [f32; 2], scale: f32) -> [f32; 2] {
    [x * scale, y * scale]
}
